Skip to content

Add iOS .stringsdict ↔ gettext PO conversion actions#739

Draft
jkmassel wants to merge 1 commit into
trunkfrom
jkmassel/ios-stringsdict-pot-conversion
Draft

Add iOS .stringsdict ↔ gettext PO conversion actions#739
jkmassel wants to merge 1 commit into
trunkfrom
jkmassel/ios-stringsdict-pot-conversion

Conversation

@jkmassel

@jkmassel jkmassel commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Round-trips iOS .stringsdict plural files through gettext .po/.pot so plurals can be translated in GlotPress — today's .strings pipeline is key/value only and can't represent plurals.

What it adds

  • ios_generate_pot_from_stringsdict (forward): English .stringsdict.pot; each plural variable becomes one msgid/msgid_plural entry, keyed by msgctxt = the stringsdict key (key:variable for multi-variable entries).
  • ios_generate_stringsdict_from_po (reverse): translated .po + the English .stringsdict as a structural template (the .po carries only strings) → per-locale .stringsdict. Maps indexed msgstr[N] to CLDR category names, back-fills the iOS-required other, and returns the contexts left untranslated (filled from the English source).
  • Helpers Ios::StringsdictHelper (plist I/O + conversions) and Ios::PluralRules (locale → ordered categories, generated).

CLDR support — why it isn't "full"

iOS plural selection is CLDR-based, so "fully correct" would mean full CLDR. We deliberately match GlotPress instead, because it produces the .po and lags CLDR for several locales (Romance stays 2-form; Hebrew 2 vs 3; Welsh on a legacy 4-form rule) with no fix imminent — two abandoned PRs (GlotPress/GlotPress#692, #784) under the still-open umbrella issue #666, and ~27% of locales skipped in GlotPress's own CLDR test. So Ios::PluralRules is generated from GlotPress GP_Locales (form count) × CLDR (category names); being "more CLDR than GlotPress" would expect forms the .po never contains. Regenerate via rakelib/generate_ios_plural_rules.rb (vendored inputs).

Across all 29 WordPress-iOS locales, 27 produce correct plurals (Indonesian harmlessly over-supplies an inert form). The two exceptions:

  • Welsh (cy) — its legacy 4-form rule can't map to CLDR's six categories, so the converter raises a clean error; exclude cy from the locales you convert (in-app, Welsh then uses its non-plural .strings entry).
  • Hebrew (he) — 2-form vs CLDR's three, so the dual isn't supplied and n = 2 renders with the plural form. Degraded, not wrong.

Both self-heal: the reverse converter reads nplurals from each .po's Plural-Forms header and fails loud if it stops matching the table — so when GlotPress fixes a locale, the pipeline tells us to regenerate.

Test plan

  • bundle exec rspec — 39 examples across the 4 new specs, 0 failures
  • bundle exec rubocop — no offenses
  • Full suite — 939 examples, 0 failures
  • rakelib/generate_ios_plural_rules.rb reproduces the committed table exactly

Notes

No new runtime dependencies — plist/gettext were already in the gemspec; vendored CLDR/GlotPress data lives under rakelib/ (dev-only, not shipped). The diff is over Danger's 500-line soft limit, but it's one cohesive feature.

@dangermattic

Copy link
Copy Markdown
Collaborator
1 Warning
⚠️ This PR is larger than 500 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.
1 Message
📖 This PR is still a Draft: some checks will be skipped.

Generated by 🚫 Danger

jkmassel added a commit that referenced this pull request Jun 21, 2026
jkmassel added a commit that referenced this pull request Jun 22, 2026
Round-trip iOS `.stringsdict` plural files through gettext `.po`/`.pot` so
plurals can be translated in a system like GlotPress — the existing `.strings`
pipeline is key/value only and can't represent plurals.

- `ios_generate_pot_from_stringsdict` (forward): English `.stringsdict` → `.pot`,
  one `msgid`/`msgid_plural` entry per plural variable (`msgctxt` is the key, or
  `key:variable` for multi-variable entries).
- `ios_generate_stringsdict_from_po` (reverse): translated `.po` + the English
  `.stringsdict` as a structural template → per-locale `.stringsdict`, mapping
  indexed `msgstr[N]` back to CLDR category names and back-filling the
  iOS-required `other`. Returns the contexts left untranslated (kept as English).

`Ios::PluralRules` maps a locale to its ordered CLDR categories. Because the
pipeline consumes GlotPress `.po` exports and GlotPress lags CLDR for several
locales, the table is generated (`rakelib/generate_ios_plural_rules.rb`) from
GlotPress `GP_Locales` (form count) × CLDR (category names) rather than from
CLDR alone; the reverse converter reads each `.po`'s `Plural-Forms` header and
fails loud if the count drifts from the table. Locales whose GlotPress rule
can't map to CLDR (e.g. Welsh) fail with a clean user error.

New helpers `Ios::StringsdictHelper` and `Ios::PluralRules`; no new runtime
dependencies (`plist`/`gettext` were already in the gemspec). [#739]
@jkmassel jkmassel force-pushed the jkmassel/ios-stringsdict-pot-conversion branch from b55e3e4 to fe9b425 Compare June 22, 2026 21:36
Round-trip iOS `.stringsdict` plural files through gettext `.po`/`.pot` so
plurals can be translated in a system like GlotPress — the existing `.strings`
pipeline is key/value only and can't represent plurals.

- `ios_generate_pot_from_stringsdict` (forward): English `.stringsdict` → `.pot`,
  one `msgid`/`msgid_plural` entry per plural variable (`msgctxt` is the key, or
  `key:variable` for multi-variable entries).
- `ios_generate_stringsdict_from_po` (reverse): translated `.po` + the English
  `.stringsdict` as a structural template → per-locale `.stringsdict`, mapping
  indexed `msgstr[N]` back to CLDR category names and back-filling the
  iOS-required `other`. Returns the contexts left untranslated (kept as English).

`Ios::PluralRules` maps a locale to its ordered CLDR categories. Because the
pipeline consumes GlotPress `.po` exports and GlotPress lags CLDR for several
locales, the table is generated (`rakelib/generate_ios_plural_rules.rb`) from
GlotPress `GP_Locales` (form count) × CLDR (category names) rather than from
CLDR alone; the reverse converter reads each `.po`'s `Plural-Forms` header and
fails loud if the count drifts from the table. Locales whose GlotPress rule
can't map to CLDR (e.g. Welsh) fail with a clean user error.

New helpers `Ios::StringsdictHelper` and `Ios::PluralRules`; no new runtime
dependencies (`plist`/`gettext` were already in the gemspec). [#739]
@jkmassel jkmassel force-pushed the jkmassel/ios-stringsdict-pot-conversion branch from fc2b121 to ee18adb Compare June 23, 2026 19:47
@iangmaia iangmaia requested a review from Copilot July 13, 2026 19:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class support in Release Toolkit for round-tripping iOS .stringsdict plural resources through gettext .pot/.po, enabling plural translations via GlotPress while preserving .stringsdict structure via an English template and locale-specific plural category mapping.

Changes:

  • Introduces ios_generate_pot_from_stringsdict and ios_generate_stringsdict_from_po Fastlane actions for forward/reverse conversion between .stringsdict and gettext.
  • Adds iOS helper modules for .stringsdict plist I/O + conversion logic and a generated locale→CLDR-category mapping table aligned to GlotPress plural-form counts.
  • Adds fixtures, generator inputs, and RSpec coverage for conversions, plural-category mapping, and failure modes (unmapped locales, drift in plural form counts, partial translations).

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
spec/test-data/translations/stringsdict/simple.stringsdict Fixture for a single-variable .stringsdict plural entry.
spec/test-data/translations/stringsdict/Localizable.stringsdict Fixture covering multi-entry and multi-variable .stringsdict cases.
spec/ios_stringsdict_helper_spec.rb Unit tests for .stringsdict read/write + .pot generation + reverse conversion from .po.
spec/ios_plural_rules_spec.rb Unit tests validating locale normalization, category mappings, and incompatible locales.
spec/ios_generate_stringsdict_from_po_spec.rb Action-level tests for generating localized .stringsdict files from .po + template.
spec/ios_generate_pot_from_stringsdict_spec.rb Action-level tests for generating .pot templates from .stringsdict.
rakelib/plural_rules_data/glotpress_nplurals.json Vendored GlotPress plural-form counts (input to rule-table generator).
rakelib/plural_rules_data/cldr_plurals.xml Vendored CLDR plural categories data (input to rule-table generator).
rakelib/generate_ios_plural_rules.rb Generator script combining GlotPress nplurals + CLDR category names into the runtime table.
lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_stringsdict_helper.rb Core conversion logic between .stringsdict and gettext .po/.pot.
lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_plural_rules.rb Generated locale→ordered plural-category mapping aligned to GlotPress outputs.
lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_generate_stringsdict_from_po.rb Fastlane action wrapper for reverse conversion (.po → localized .stringsdict).
lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_generate_pot_from_stringsdict.rb Fastlane action wrapper for forward conversion (English .stringsdict.pot).
CHANGELOG.md Documents the new iOS plural round-trip actions and behavior/limitations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +205 to +207
def self.context_for(key:, variable:, single_variable:)
single_variable ? key : "#{key}:#{variable}"
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants